# @Time   : 2022-03-03 17:00:00
# @Author : Gavin Jiang
# @Owner  : YSRD (Insights Value)

import importlib
import inspect
import os
import re

from flask import Flask, Blueprint
from flask_restful import Api

from framework.interface.abstract_api import AbstractApi
from framework.the_path import ThePath


class RegisterRouter:

    def __get_default_url_name(self, cls_name):
        p = re.compile(r'([a-z]|\d)([A-Z])')
        return re.sub(p, r'\1-\2', cls_name).lower()

    def __get_package_cls_data(self, file_name, package_name):
        pkg_cls_data = []
        package_name = package_name.replace('\\', '/')
        if file_name.endswith('.py') and not file_name.startswith('__init__'):
            module = importlib.import_module(f"{package_name.replace('/','.')}.{file_name[:-3]}")
            cls_list = inspect.getmembers(module, inspect.isclass)
            for cls_name, cls in cls_list:
                if cls != AbstractApi and issubclass(cls, AbstractApi):
                    pkg_cls_data.append({'pkg_name': package_name, 'cls_name': cls_name, 'cls': cls})

        return pkg_cls_data

    def __register_api(self, cls_datum, restful_api: Api):
        cls = cls_datum.get('cls')
        url_names = getattr(cls, 'url_names')()

        if url_names:
            if isinstance(url_names, str):
                restful_api.add_resource(cls, f"/{url_names}")
            else:
                restful_api.add_resource(cls, *[f"/{url_name}" for url_name in url_names])
        else:
            restful_api.add_resource(cls, f"/{self.__get_default_url_name(cls_datum.get('cls_name'))}")

    def create_init_file(self, path):
        init_file = os.path.join(path, '__init__.py')
        if not os.path.exists(init_file):
            with open(os.path.join(path, '__init__.py'), 'a') as f:
                f.write('')

    def register_router(self, app: Flask):
        """
        @Time   : 2022-05-24 12:00:00
        @Author : Feihong Wang
        @Owner  : YSRD (Insights Value)

        曹科在运行pytest时,
        如果app下某个文件夹中没有__init__.py文件
        Blueprint(blueprint_name, package_name)会报错:
        TypeError: expected str, bytes or os.PathLike object, not NoneType
        而执行runserver文件时不会如此

        梳理一遍原因：
        Blueprint中获取root_path的关键代码：

        mod = sys.modules.get(import_name)
        if mod is not None and hasattr(mod, "__file__"):
            return os.path.dirname(os.path.abspath(mod.__file__))

        if hasattr(loader, "get_filename"):
            filepath = loader.get_filename(import_name)

        原因是,pytest的每个测试文件都会通过contest.py执行create_app('test')
        所以会执行多次register_router函数
        进而多次执行这个函数:Blueprint(blueprint_name, package_name),
        查看 Blueprint 源码，发现初始化时需要 运算root_path变量
        第一次 Blueprint 检测到 package_name 未被import
        sys.modules.get(package_name)返回None，
        最终Blueprint.root_path 会得到loader.get_filename(import_name),且不报错
        接下来执行 self.__get_package_cls_data(file, package_name)，模块被导入
        sys.modules.get(package_name)得到这种类型的数据:
        模块文件夹有__init__.py文件会获得这样的root_path:
        <module 'app.demo' from '/Users/wangfeihong/Desktop/yard-base/src/app/demo/__init__.py'>
        如果文件夹中没有__init__.py会获得这样的root_path:
        <module 'app.vtestt.action' (namespace)>，
        这种由于没有__init__.py文件也不会被self.__get_package_cls_data当成模块引入

        第二次再执行 Blueprint(blueprint_name, package_name),
        此时 package_name 已经被导入，mod = sys.modules.get(import_name)!=None,
        root_path 为 os.path.dirname(os.path.abspath(mod.__file__))
        由于 <module 'app.vtestt.action' (namespace)>(命名空间的模块，和普通模块的区别应该就是没有__init__.py)
        这种类型没有__file__属性，所以报错 TypeError: expected str, bytes or os.PathLike object, not NoneType

        两种解决方法
        1. Blueprint(blueprint_name, package_name, root_path='')，暂时不知道root_path有什么用，不过程序运行正常
        2. 检测文件夹有没有__init__.py文件,如果没有则新建
        """
        app_path = ThePath.app()
        src_path = ThePath.src()

        for dir_path, dir_names, files in os.walk(app_path):

            package_name = dir_path.replace(src_path, '').replace('\\', '.').replace('/', '.')[1:]
            blueprint_name = dir_path.replace(app_path, '').replace('\\', '/')
            if '__pycache__' in blueprint_name or blueprint_name == '':
                continue

            self.create_init_file(dir_path)
            blueprint = Blueprint(blueprint_name, package_name)

            restful_api = Api(blueprint)


            for file in files:
                pkg_cls_data = self.__get_package_cls_data(file, package_name)

                if len(pkg_cls_data) == 0:
                    continue

                for cls_datum in pkg_cls_data:
                    self.__register_api(cls_datum, restful_api)

            if len(restful_api.endpoints) > 0:
                app.register_blueprint(blueprint, url_prefix=f"/{blueprint_name}")
